/* * Copyright 2012 b1.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.b1.pack.standard.reader; import com.google.common.base.Charsets; import com.google.common.base.Preconditions; import com.google.common.io.ByteStreams; import com.google.common.io.CountingInputStream; import com.google.common.primitives.Bytes; import com.google.common.primitives.Ints; import org.b1.pack.api.reader.ReaderProvider; import org.b1.pack.api.reader.ReaderVolume; import org.b1.pack.standard.common.*; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.ExecutorService; class VolumeCursor implements Closeable { public static final String VOLUME_BROKEN_MESSAGE = "Volume broken or not a B1 archive"; private static final String VOLUME_BROKEN_PATTERN = VOLUME_BROKEN_MESSAGE + ": %s"; private static final int MAX_TAIL_SIZE = 1024; private final ReaderProvider provider; private ExecutorService executorService; private PackCipher packCipher; private VolumeCipher volumeCipher; private String archiveId; private Long objectTotal; private RecordPointer catalogPointer; private long volumeNumber; private ReaderVolume volume; private HeaderSet headerSet; private CountingInputStream inputStream; public VolumeCursor(ReaderProvider provider) { this.provider = provider; } public void initialize() throws IOException { openVolume(1); objectTotal = headerSet.getObjectTotal(); if (!initCatalogPointer()) { openVolume(provider.getVolumeCount()); Preconditions.checkState(initCatalogPointer(), "Catalog pointer not found"); } } public ExecutorService getExecutorService() { return executorService != null ? executorService : (executorService = provider.getExecutorService()); } public Long getObjectTotal() throws IOException { return objectTotal; } public RecordPointer getCatalogPointer() throws IOException { return catalogPointer; } public BlockPointer getBlockPointer() { return new BlockPointer(volumeNumber, inputStream.getCount()); } public InputStream getInputStream() throws IOException { return inputStream; } public VolumeCipher getVolumeCipher() { return volumeCipher; } public void seek(BlockPointer pointer) throws IOException { if (pointer.volumeNumber == volumeNumber) { long skipCount = pointer.blockOffset - inputStream.getCount(); if (skipCount >= 0) { ByteStreams.skipFully(inputStream, skipCount); return; } } openVolume(pointer.volumeNumber); long skipCount = pointer.blockOffset - inputStream.getCount(); Preconditions.checkState(skipCount >= 0); ByteStreams.skipFully(inputStream, skipCount); } public void next() throws IOException { openVolume(volumeNumber + 1); } @Override public void close() throws IOException { try { if (inputStream != null) inputStream.close(); } finally { if (executorService != null) executorService.shutdown(); } } private boolean initCatalogPointer() throws IOException { return (catalogPointer = headerSet.getCatalogPointer()) != null || (catalogPointer = readTail().getCatalogPointer()) != null; } private void openVolume(long number) throws IOException { if (inputStream != null) { inputStream.close(); } volumeNumber = number; volume = Preconditions.checkNotNull(provider.getVolume(number), "Volume %s not found", number); inputStream = new CountingInputStream(volume.getInputStream()); headerSet = readHead(number); checkVolume(headerSet.getSchemaVersion() != null); Preconditions.checkState(headerSet.getSchemaVersion() <= Volumes.SCHEMA_VERSION, "B1 archive version not supported (%s): %s", headerSet.getSchemaVersion(), volume.getName()); checkVolume(headerSet.getArchiveId() != null && headerSet.getArchiveId().equals(archiveId)); checkVolume(headerSet.getVolumeNumber() != null && headerSet.getVolumeNumber() == volumeNumber); } private HeaderSet readHead(long volumeNumber) throws IOException { String signature = volumeNumber == 1 ? Volumes.B1_AS : Volumes.B1_VS; ByteArrayOutputStream buffer = new ByteArrayOutputStream(); while (true) { int b = inputStream.read(); checkVolume(b != -1); if ((byte) b == Volumes.SEPARATOR_BYTE) break; buffer.write(b); if (buffer.size() == signature.length()) { checkVolume(buffer.toString(Charsets.UTF_8.name()).equals(signature)); } } checkVolume(buffer.size() > signature.length()); HeaderSet headerSet = new HeaderSet(buffer.toString(Charsets.UTF_8.name())); Integer iterationCount = headerSet.getIterationCount(); if (archiveId == null) { archiveId = headerSet.getArchiveId(); checkVolume(archiveId != null); if (iterationCount != null) { packCipher = new PackCipher(provider.getPassword(), Volumes.decodeBase64(archiveId), iterationCount); } } else { if (iterationCount == null) { checkVolume(packCipher == null); } else { checkVolume(packCipher != null && packCipher.getIterationCount() == iterationCount); } } if (packCipher == null) { return headerSet; } byte[] encryptedHeaders = headerSet.getEncryptedHeaders(); checkVolume(encryptedHeaders != null); volumeCipher = packCipher.getVolumeCipher(volumeNumber); String plaintext = new String(volumeCipher.cipherHead(false, encryptedHeaders), Charsets.UTF_8.name()); checkVolume(plaintext.startsWith(signature)); return new HeaderSet(plaintext); } private HeaderSet readTail() throws IOException { long available = Preconditions.checkNotNull(volume.getSize(), "Volume size unknown") - inputStream.getCount(); int capacity = Ints.checkedCast(Math.min(available, MAX_TAIL_SIZE)); ByteStreams.skipFully(inputStream, available - capacity); MemoryOutputStream outputStream = new MemoryOutputStream(capacity); ByteStreams.copy(inputStream, outputStream); Preconditions.checkState(outputStream.size() == capacity); int index = Bytes.lastIndexOf(outputStream.getBuf(), Volumes.SEPARATOR_BYTE) + 1; checkVolume(index > 0); String result = new String(outputStream.getBuf(), index, capacity - index, Charsets.UTF_8.name()); checkVolumeEnd(result); HeaderSet headerSet = new HeaderSet(result); if (packCipher == null) { return headerSet; } byte[] encryptedHeaders = headerSet.getEncryptedHeaders(); checkVolume(encryptedHeaders != null); String plaintext = new String(volumeCipher.cipherTail(false, encryptedHeaders), Charsets.UTF_8.name()); checkVolumeEnd(plaintext); return new HeaderSet(plaintext); } private void checkVolumeEnd(String result) { checkVolume(result.endsWith(Volumes.B1_AE) || result.endsWith(Volumes.B1_VE)); } private void checkVolume(boolean expression) { Preconditions.checkState(expression, VOLUME_BROKEN_PATTERN, volume.getName()); } }